﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using ch07.binary.module;
using ch07.binary.type;

namespace ch07.binary.wasm_reader
{
    public partial struct WasmReader
    {
        internal byte[] Data;

        private static ( Module, Exception ) Decode(byte[] data)
        {
            var module = new Module();
            try
            {
                var reader = new WasmReader {Data = data};

                reader.ReadModule(ref module);
                return (module, null);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                return (module, e);
            }
        }

        public static ( Module, Exception ) DecodeFile(string filename)
        {
            try
            {
                var data = File.ReadAllBytes(filename);
                return Decode(data);
            }
            catch (Exception e)
            {
                return (new Module(), e);
            }
        }

        public int Remaining()
        {
            return Data.Length;
        }

        // fixed length value
        public byte ReadByte()
        {
            if (Data.Length < 1)
            {
                Panic.Run(Errors.ErrUnexpectedEnd);
            }

            var b = Data[0];
            Data = Data.Skip(1).ToArray();
            return b;
        }

        public uint ReadU32()
        {
            if (Data.Length < 4)
            {
                Panic.Run(Errors.ErrUnexpectedEnd);
            }

            var n = BitConverter.ToUInt32(Data);
            Data = Data.Skip(4).ToArray();
            return n;
        }

        public float ReadF32()
        {
            if (Data.Length < 4)
            {
                Panic.Run(Errors.ErrUnexpectedEnd);
            }

            var n = BitConverter.ToSingle(Data);
            Data = Data.Skip(4).ToArray();
            return n;
        }

        public double ReadF64()
        {
            if (Data.Length < 8)
            {
                Panic.Run(Errors.ErrUnexpectedEnd);
            }

            var n = BitConverter.ToDouble(Data);
            Data = Data.Skip(8).ToArray();
            return n;
        }

        // variable length value
        public uint ReadVarU32()
        {
            var (n, w) = LEB128.DecodeVarUint(Data, 32);
            Data = Data.Skip(w).ToArray();
            return (uint) n;
        }

        public int ReadVarS32()
        {
            var (n, w) = LEB128.DecodeVarInt(Data, 32);
            Data = Data.Skip(w).ToArray();
            return (int) n;
        }

        public long ReadVarS64()
        {
            var (n, w) = LEB128.DecodeVarInt(Data, 64);
            Data = Data.Skip(w).ToArray();
            return n;
        }

        // bytes & name
        public byte[] ReadBytes()
        {
            //先读取长度
            var n = ReadVarU32();
            if (Data.Length < (int) (n))
            {
                Panic.Run(Errors.ErrUnexpectedEnd);
            }

            var bytes = Data.Take((int) n).ToArray();
            Data = Data.Skip((int) n).ToArray();
            return bytes;
        }

        public string ReadName()
        {
            var data = ReadBytes();
            var name = Encoding.UTF8.GetString(data);
            if (!Encoding.UTF8.GetBytes(name).SequenceEqual(data))
            {
                Panic.Run("malformed UTF-8 encoding");
            }

            return name;
        }

        private Limits ReadLimits()
        {
            var limits = new Limits
            {
                Tag = ReadByte(),
                Min = ReadVarU32()
            };
            if (limits.Tag == 1)
            {
                limits.Max = ReadVarU32();
            }

            return limits;
        }

        // module
        private void ReadModule(ref Module module)
        {
            if (Remaining() < 4)
            {
                Panic.Run("unexpected end of magic header");
            }

            module.Magic = ReadU32();
            if (module.Magic != ConstData.MagicNumber)
            {
                Panic.Run("magic header not detected");
            }

            if (Remaining() < 4)
            {
                Panic.Run("unexpected end of binary version");
            }

            module.Version = ReadU32();
            if (module.Version != ConstData.Version)
            {
                Panic.Run("unknown binary version: {0:D}", module.Version);
            }

            ReadSections(ref module);
            if (module.FuncSec.Length != module.CodeSec.Length)
            {
                Panic.Run("function and code section have inconsistent lengths");
            }

            if (Remaining() > 0)
            {
                Panic.Run("junk after last section");
            }
        }

        private CustomSec ReadCustomSec()
        {
            var secReader = new WasmReader {Data = ReadBytes()};
            return new CustomSec
            {
                Name = secReader.ReadName(),
                Bytes = secReader.Data // TODO
            };
        }

        // sections
        private void ReadSections(ref Module module)
        {
            var prevSecID = (byte) 0;
            //需要初始化空数组
            module.CustomSecs ??= Array.Empty<CustomSec>();
            for (; Remaining() > 0;)
            {
                var secID = ReadByte();
                switch (secID)
                {
                    case ConstData.SecCustomID:
                    {
                        var tail = ReadCustomSec();
                        module.CustomSecs = module.CustomSecs.Append(tail).ToArray();
                        continue;
                    }
                    case > ConstData.SecDataID:
                        Panic.Run("malformed section id: %d", secID);
                        break;
                }

                if (secID <= prevSecID)
                {
                    Panic.Run("junk after last section, id: %d", secID);
                }

                prevSecID = secID;

                var n = ReadVarU32();
                var remainingBeforeRead = Remaining();
                ReadNonCustomSec(secID, ref module);
                if (Remaining() + (int) (n) != remainingBeforeRead)
                {
                    Panic.Run("section size mismatch, id: %d", secID);
                }
            }
        }

        private void ReadNonCustomSec(byte secID, ref Module module)
        {
            switch (secID)
            {
                case ConstData.SecTypeID:
                    module.TypeSec = ReadTypeSec();
                    break;
                case ConstData.SecImportID:
                    module.ImportSec = ReadImportSec();
                    break;
                case ConstData.SecFuncID:
                    module.FuncSec = ReadIndices();
                    break;
                case ConstData.SecTableID:
                    module.TableSec = ReadTableSec();
                    break;
                case ConstData.SecMemID:
                    module.MemSec = ReadMemSec();
                    break;
                case ConstData.SecGlobalID:
                    module.GlobalSec = ReadGlobalSec();
                    break;
                case ConstData.SecExportID:
                    module.ExportSec = ReadExportSec();
                    break;
                case ConstData.SecStartID:
                    module.StartSec = ReadStartSec();
                    break;
                case ConstData.SecElemID:
                    module.ElemSec = ReadElemSec();
                    break;
                case ConstData.SecCodeID:
                    module.CodeSec = ReadCodeSec();
                    break;
                case ConstData.SecDataID:
                    module.DataSec = ReadDataSec();
                    break;
            }
        }
    }
}